Skip to content

Conversation

@hadley
Copy link
Member

@hadley hadley commented Jul 28, 2025

expect_success() and expect_failure() are now stricter, testing the full set of assumptions (i.e. exactly one success/failure and zero failures/successes). This means that expect_no_successes() and expect_no_failures() can be deprecated.

Fixes #2110

@lionel- could you please take a look at my restarts and reassure yourself that I'm using them correctly?

@MichaelChirico I'd love any thoughts on the documentation. It's going to take a few PRs to dig myself out of this hole, so no guarantees I'll make any bigger changes here, but I'll definitely consider them for future work.

hadley added 5 commits July 28, 2025 08:47
`expect_success()` and `expect_failure()` are now stricter, testing the full set of assumptions (i.e. exactly one success/failure and zero failures/successes). This means that `expect_no_successes()` and `expect_no_failures()` can be deprecated.

Fixes #2110
#' @param message Check that the failure message matches this regexp.
#' @param ... Other arguments passed on to [expect_match()].
#' @export
expect_success <- function(expr) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems likely to be a breaking change. I guess you have a system for revdep checking before release, but it might be worth trying out on a sample of 50/100 packages now to see how big the impact might be.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking at https://github.com/search?q=org%3Acran+expect_success+path%3A%2F%5Etests%5C%2Ftestthat%5C%2F%2F&type=code suggests that there will be a few packages that this breaks, but most of the uses of expect_success() look incorrect and think PRs that just remove expect_success() shouldn't be too hard for me to do.

"Creating reference"
)
)
expect_known_output(cat("ok!\n"), file) |>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the 4.1.0 requirement is so satisfying 🧘‍♂️

Copy link
Member Author

@hadley hadley Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The simple joys of being able to use R features that were released 5 years ago 😆


test_that("succeed always succeeds", {
expect_success(succeed())
expect_failure(expect_failure({}))
Copy link
Contributor

@MichaelChirico MichaelChirico Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i wonder if it might improve readability in this file in particular to alias expect_failure, something like meta_expect_failure = expect_failure, then any given test will look more like "we're testing the behavior of expect_failure" more clearly by not repeating the same name.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I worry that would imply there's an independent implementation rather than using the expectation to test itself (which does seem dangerous, but I can't see a better way currently).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to clarify I would define the alias at the top of the file, or perhaps even inside each test_that(), so that it's perfectly clear it's just a copy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll continue to contemplate that as I iterate, but I'm going to leave it off this PR for now.

expect_success(succeed())
expect_failure(expect_failure({}))
expect_failure(expect_failure(succeed()))
expect_failure(expect_failure({
Copy link
Contributor

@MichaelChirico MichaelChirico Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a bit torn but worth considering adding the parallel test with two fail()s, even though the second can't be reached?

(edit: I see it's done below in direct tests of capture_success_failure(), my taste is to channel through user-facing APIs where possible)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I think that's a good place to reinforce that we should only ever see one fail


## Testing your expectations

testthat comes with three expectations designed specifically to test expectations: `expect_success()` and `expect_failure()`:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To me this is all downstream of the principle you conveyed in #1469 -- custom expectations should only give one pass/fail condition. I'm not sure that's conveyed here.

A simple example like what I'd tried in #1469 might be illustrative: two versions of a custom expectation which superficially look equivalent, but the bad one uses multiple succeed() and the preferred one uses if (!...) fail() instead.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, agreed. My plan is to discuss that more in #2125

Copy link
Member

@lionel- lionel- left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM although internal variables might be a little clearer

Comment on lines +7 to +16
last_failure <- NULL

withRestarts(
withCallingHandlers(
expr,
expectation_failure = function(cnd) {
last_failure <<- cnd
n_failure <<- n_failure + 1
# Finish the test without bubbling up
invokeRestart("failed")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIUC n_failure is either 0 or 1, because we're unwinding back to failed at the first failure?

In this case I think it would be a little clearer to remove n_failure and rename last_failure (whose "last" prefix imply there might be more than one assigned here) to just failure. Then you test for !is.null(failure) instead of n_failure > 0.

Also fine as is but then I'd add a comment saying we expect at most 1 failure.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a good suggestion but I'm going to leave it until after #2129 because there I am encountering a weird situation where fail() is not causing an expectation to exit and I think that this might help me debug it.

Comment on lines +78 to +80
} else if (status$n_failure > 1) {
# This should be impossible, but including for completeness
fail("Expectation failed more than once")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh yeah then you wouldn't need this branch

hadley and others added 2 commits July 29, 2025 07:50
@hadley hadley merged commit cc79388 into main Jul 29, 2025
10 of 11 checks passed
@hadley hadley deleted the improved-expectation-testing branch July 29, 2025 13:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

expect_success() and expect_failure() potentially misleading under multiple conditions

4 participants